package pacman.entries.pacman;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Random;

import pacman.entries.pacman.ml.destination_features.CanBeatGhostsDestinationFeature;
import pacman.entries.pacman.ml.destination_features.ReturnOneFeature;
import pacman.entries.pacman.ml.destination_features.DestinationFeature;
import pacman.entries.pacman.ml.destination_features.DistanceDestinationFeature;
import pacman.entries.pacman.ml.destination_features.ThreatFromGhostsDestinationFeature;
import pacman.game.Game;
import pacman.game.Constants.DM;
import pacman.game.Constants.MOVE;
import pacman.game.internal.Maze;
import pacman.game.internal.Node;

public class DestinationLearningPacMan extends LearningPacMan {

	public DestinationLearningPacMan() {
		super();
		
		InitTestWeights();
	}
	
	// ______________________________________________ DECISION MAKING
	
	// holds pill counts in each possible direction to the next corner, and the adjacent corners
	class CornerNode {
		public int index;
		public int[] pillCounts;
		public int[] adjacentCorners; // store a list of CornerNode objects
		public CornerNode[] adjacentCornersNodes;
		public boolean found;
		public CornerNode() {
			pillCounts = new int[4];
			adjacentCorners = new int[4];
			Arrays.fill(pillCounts, -1);
			Arrays.fill(adjacentCorners, -1);
			Arrays.fill(adjacentCornersNodes, null);
			found = false;
		}
		// move is being used as the direction from the corner
		// returns -1 if there is no corner in the given direction
		public int getPillCount(MOVE move) {
			return pillCounts[move.ordinal()];
		}
		public int decrementPillCount(MOVE move) {
			return --(pillCounts[move.ordinal()]);
		}
		public int getCorner(MOVE move) {
			return adjacentCorners[move.ordinal()];
		}
		public CornerNode getCornerNode(MOVE move) {
			return adjacentCornersNodes[move.ordinal()];
		}
	}
	HashMap<Integer, HashMap<MOVE, CornerNode>> m_NearestCorners;  // for each non-corner node, stores the 2 corners that contain it.
	HashMap<Integer, CornerNode> m_CornerIndexes;
	
	private void FindCorners(Game game) {
		m_CornerIndexes = new HashMap<Integer, CornerNode>();
		for (Node node : game.getCurrentMaze().graph) {
			int index = node.nodeIndex;
			if (IsCorner(game, index)) {
				m_DestinationIndexes.add(new Integer(index));
				AddCornerIndex(index);
			}
		}
	}
	
	private void AddCornerIndex(int index) {
		m_CornerIndexes.put(new Integer(index), new CornerNode());
	}
	
	private void BuildNearestCornersMap(Game game) {
		m_NearestCorners = new HashMap<Integer, HashMap<MOVE, CornerNode>>();
		CornerNode start = ((CornerNode[])m_CornerIndexes.values().toArray(new CornerNode[0]))[0];
		BuildNearestCornersMap(start, game);
	}
	private void BuildNearestCornersMap(CornerNode corner, Game game) {
		corner.found = true;
		Maze maze = game.getCurrentMaze();
		int cornerIndex = corner.index;
		Node cornerNodeType = maze.graph[cornerIndex];
		for(MOVE move : MOVE.values()) {
			// int nextCorner = corner.getCorner(move);
			CornerNode nextCornerNode = corner.getCornerNode(move);
			if(nextCornerNode != null) {
				continue;  // this edge has already been traversed
			}
			int next = game.getNeighbour(cornerIndex, move);
			MOVE nextMove;
			int previous = cornerIndex;
			MOVE previousMove = game.getMoveToMakeToReachDirectNeighbour(next, previous);
			if(next < 0) {
				continue;
			}
			ArrayList<Integer> intermediates = new ArrayList<Integer>(100);
			// stores the move towards the next corner, and towards the previous corner, for each node.
			// first move is towards the previous corner, second move is towards the next corner
			ArrayList<MOVE[]> intermediateMoves = new ArrayList<MOVE[]>(100); 
			
			int pillCount = 0;
			while(!m_CornerIndexes.containsKey(next)) {
				intermediates.add(next);
				int pillIndex = game.getPillIndex(next);
				if(pillIndex > -1) pillCount++;    // add to pill count if this node is a pill
				// for(int adjacentNode : maze.graph[next].neighbourhood.values() ) {
				for(Entry<MOVE, Integer> adjacentNode : maze.graph[next].neighbourhood.entrySet() ) {
					// assume that the nodes are not junctions (they should all have been added to m_CornerIndexes)
					int adjacentNodeIndex = adjacentNode.getValue();
					nextMove = adjacentNode.getKey();
					if(previous != adjacentNodeIndex) {
						previous = next;
						next = adjacentNodeIndex;
						intermediateMoves.add(new MOVE[]{previousMove, nextMove});
						break;
					}
				}
			}
			// now the next junction has been reached.  it is stored in "next"
			int nextCorner = next;
			nextCornerNode = m_CornerIndexes.get(nextCorner);
			corner.adjacentCorners[move.ordinal()] = nextCorner;
			corner.adjacentCornersNodes[move.ordinal()] = nextCornerNode;
			MOVE oppositeMove = game.getMoveToMakeToReachDirectNeighbour(nextCorner, previous);
			nextCornerNode.adjacentCorners[oppositeMove.ordinal()] = cornerIndex;
			nextCornerNode.adjacentCornersNodes[oppositeMove.ordinal()] = corner;
			// add all the intermediate nodes to the m_NearestCorners map
			Iterator<MOVE[]> moveIterator = intermediateMoves.iterator();
			for(int intermediate : intermediates) {
				MOVE[] moves = moveIterator.next();
				HashMap<MOVE, CornerNode> movemap = new HashMap<MOVE, CornerNode>();
				movemap.put(moves[0], corner);
				movemap.put(moves[1], nextCornerNode);
				// m_NearestCorners.put(intermediate, new CornerNode[]{corner, nextCornerNode});
				m_NearestCorners.put(intermediate, movemap);
			}
			// continue traversing the corners
			if(!nextCornerNode.found) {
				BuildNearestCornersMap(nextCornerNode, game); 
			}
		}
	}
	
	HashSet<Integer> m_DestinationIndexes;
	//boolean[] m_IsActivePillArray;
	boolean[] m_IsActivePowerPillArray;
	
	int m_CurrentLevel = -1;
	
	@Override
	public MOVE getMove(Game game, long timeDue) {
		
		ChooseMove(game);
		
		return m_Move;
	}
	
	boolean t_printed = false;
	
	protected void ChooseMove(Game game) {
		
		if (game.getCurrentLevel() != m_CurrentLevel) {
			FindDestinations(game);
			m_CurrentLevel = game.getCurrentLevel();
		}
		
		// check active power pills and pills
		UpdateActivePills(game);
		
		// for each possible destination, evaluate it based on its features/weights, remembering
		// the best one
		int bestDestination = 0;
		float bestScore = Float.MIN_VALUE;
		for (Integer indexInteger : m_DestinationIndexes) {
			int index = indexInteger.intValue();
			float score = EvaluateDestination(game, index);
			
			if (score > bestScore) {
				bestScore = score;
				bestDestination = index;
			}
		}
		
		t_printed = true;
		
		// return the move toward that destination
		m_Move = game.getNextMoveTowardsTarget(game.getPacmanCurrentNodeIndex(), 
				bestDestination, DM.PATH);
	}
	
	private void UpdateActivePills(Game game) {
		//Arrays.fill(m_IsActivePillArray, false);
		Arrays.fill(m_IsActivePowerPillArray, false);

		int[] powerPills = game.getActivePowerPillsIndices();
		for (int index : powerPills)
			m_IsActivePowerPillArray[index] = true;
		
		/*int[] pills = game.getActivePillsIndices();
		for (int index : pills)
			m_IsActivePillArray[index] = true;*/
	}
	
	private float EvaluateDestination(Game game, int index) {
		
		if (index == game.getPacmanCurrentNodeIndex())
			return Float.MIN_VALUE;
		
		float score = 0.0f;

		// evaluate general destination features
		for (int i = 0; i < m_DestinationFeatures.size(); i++) {
			score += m_DestinationWeights[i] * 
				m_DestinationFeatures.get(i).EvaluateDestination(game, index);
		}
		
		// evaluate Junction features
		if (game.isJunction(index))
			for (int i = 0; i < m_JunctionFeatures.size(); i++) {
				score += m_JunctionWeights[i] * 
					m_JunctionFeatures.get(i).EvaluateDestination(game, index);
			}
		
		// evaluate PowerPill features
		if (index < m_IsActivePowerPillArray.length && m_IsActivePowerPillArray[index])
			for (int i = 0; i < m_PowerPillFeatures.size(); i++) {
				score += m_PowerPillWeights[i] * 
					m_PowerPillFeatures.get(i).EvaluateDestination(game, index);
			}
		
		// evaluate Pill features
		/*if (index < m_IsActivePillArray.length && m_IsActivePillArray[index])
			for (int i = 0; i < m_CornerFeatures.size(); i++) {
				score += m_CornerWeights[i] * 
					m_CornerFeatures.get(i).EvaluateDestination(game, index);
			}
		*/
		
		return score;
	}
	
	//
	private void FindDestinations(Game game) {

		m_DestinationIndexes = new HashSet<Integer>();
		
		// for each junction, PowerPill, or Pill node, add to destination list
		for (int i : game.getJunctionIndices()) {
			m_DestinationIndexes.add(new Integer(i));
		}
		
		int maxI = -1;
		for (int i : game.getPowerPillIndices()) {
			m_DestinationIndexes.add(new Integer(i));
			if (i > maxI)
				maxI = i;
		}
		m_IsActivePowerPillArray = new boolean[maxI+1];
		
		//m_CornerIndexes = new HashMap<Integer, CornerNode>();
		//m_NearestCorners = new HashMap<Integer, int[]>();
		FindCorners(game);
		BuildNearestCornersMap(game);
		/*
		for (Node node : game.getCurrentMaze().graph) {
			int index = node.nodeIndex;
			if (IsCorner(game, index)) {
				m_DestinationIndexes.add(new Integer(index));
				AddCornerIndex(index);
			}
		}
		*/
		/*maxI = -1;
		for (int i : game.getPillIndices()) {
			m_DestinationIndexes.add(new Integer(i));
			if (i > maxI)
				maxI = i;
		}
		m_IsActivePillArray = new boolean[maxI+1];*/
	}
	
	private boolean IsCorner(Game game, int index) {

		int nU = game.getNeighbour(index, MOVE.UP);
		int nD = game.getNeighbour(index, MOVE.DOWN);
		int nR = game.getNeighbour(index, MOVE.RIGHT);
		int nL = game.getNeighbour(index, MOVE.LEFT);
		
		if ( (nU == -1 && nD == -1 && nL != -1 && nR != -1) ||
				(nU != -1 && nD != -1 && nL == -1 && nR == -1) )
			return false;
		return true;
	}
	
	// ______________________________________________ GENETIC LEARNING
	
	private static Random random = new Random();
	
	public static ArrayList<LearningPacMan> Generation(ArrayList<LearningPacMan> parents, 
			int count, float randomness) {
		
		ArrayList<LearningPacMan> generation = new ArrayList<LearningPacMan>();
		
		for (LearningPacMan parent : parents)
			generation.add(parent);
		
		for (int i = 0; i < count; i++) {
			DestinationLearningPacMan destinationLearningPacMan = new DestinationLearningPacMan();
			
			for (int j = 0; j < destinationLearningPacMan.m_DestinationWeights.length; j++) {
				DestinationLearningPacMan parent = (DestinationLearningPacMan) parents.get(random.nextInt(parents.size()));
				destinationLearningPacMan.m_DestinationWeights[j] = parent.m_DestinationWeights[j];
				destinationLearningPacMan.m_DestinationWeights[j] += random.nextFloat() * randomness - randomness/2.0f;
			}
			for (int j = 0; j < destinationLearningPacMan.m_CornerWeights.length; j++) {
				DestinationLearningPacMan parent = (DestinationLearningPacMan) parents.get(random.nextInt(parents.size()));
				destinationLearningPacMan.m_CornerWeights[j] = parent.m_CornerWeights[j];
				destinationLearningPacMan.m_CornerWeights[j] += random.nextFloat() * randomness - randomness/2.0f;
			}
			for (int j = 0; j < destinationLearningPacMan.m_PowerPillWeights.length; j++) {
				DestinationLearningPacMan parent = (DestinationLearningPacMan) parents.get(random.nextInt(parents.size()));
				destinationLearningPacMan.m_PowerPillWeights[j] = parent.m_PowerPillWeights[j];
				destinationLearningPacMan.m_PowerPillWeights[j] += random.nextFloat() * randomness - randomness/2.0f;
			}
			for (int j = 0; j < destinationLearningPacMan.m_JunctionWeights.length; j++) {
				DestinationLearningPacMan parent = (DestinationLearningPacMan) parents.get(random.nextInt(parents.size()));
				destinationLearningPacMan.m_JunctionWeights[j] = parent.m_JunctionWeights[j];
				destinationLearningPacMan.m_JunctionWeights[j] += random.nextFloat() * randomness - randomness/2.0f;
			}
			
			generation.add(destinationLearningPacMan);
		}
		
		return generation;
	}
	
	// ______________________________________________ PARAMETERS
	
	float[] m_DestinationWeights;
	float[] m_CornerWeights;
	float[] m_PowerPillWeights;
	float[] m_JunctionWeights;

	List<DestinationFeature> m_DestinationFeatures = new ArrayList<DestinationFeature>(4);
	List<DestinationFeature> m_CornerFeatures = new ArrayList<DestinationFeature>(4);
	List<DestinationFeature> m_PowerPillFeatures = new ArrayList<DestinationFeature>(4);
	List<DestinationFeature> m_JunctionFeatures = new ArrayList<DestinationFeature>(4);
	
	@Override
	protected void InitTestWeights() {
		
		m_DestinationFeatures = new ArrayList<DestinationFeature>(4);
		m_CornerFeatures = new ArrayList<DestinationFeature>(4);
		m_PowerPillFeatures = new ArrayList<DestinationFeature>(4);
		m_JunctionFeatures = new ArrayList<DestinationFeature>(4);
		
		// Add Destination Features
		m_DestinationFeatures.add(new DistanceDestinationFeature());
		
		// Add Corner Features
		//m_CornerFeatures.add(new ReturnOneFeature());
		
		// Add PowerPill Features
		m_PowerPillFeatures.add(new ReturnOneFeature());
		m_PowerPillFeatures.add(new CanBeatGhostsDestinationFeature());
		m_PowerPillFeatures.add(new ThreatFromGhostsDestinationFeature());
		
		// Add Junction Features
		m_JunctionFeatures.add(new CanBeatGhostsDestinationFeature());
		m_JunctionFeatures.add(new ThreatFromGhostsDestinationFeature());
		
		// initialize weight arrays
		m_DestinationWeights = new float[m_DestinationFeatures.size()];
		m_CornerWeights = new float[m_CornerFeatures.size()];
		m_PowerPillWeights = new float[m_PowerPillFeatures.size()];
		m_JunctionWeights = new float[m_JunctionFeatures.size()];

		Arrays.fill(m_DestinationWeights, 1.0f);
		Arrays.fill(m_CornerWeights, 1.0f);
		Arrays.fill(m_PowerPillWeights, 1.0f);
		Arrays.fill(m_JunctionWeights, 1.0f);
	}
	
	@Override
	public void SaveWeights(String filename) {
		// TODO implement this
		
		//ExecutorDiagnostics.saveObject(m_Weights, filename + N + "x" + M);
	}
	
	@Override
	public void LoadWeights(String filename) {
		// TODO implement this
		
		//m_Weights = (float[][])ExecutorDiagnostics.loadObject(filename);
	}
}
